﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using Sinacor.OMM.HealthMonitor.Collector;
using Sinacor.OMM.HealthMonitor.Client;
using Sinacor.OMM.HealthMonitor.Configuration;
using Sinacor.OMM.Util;
using System.Reflection;
using PSE.Framework.Threading;

namespace Sinacor.OMM.HealthMonitor.Server.TcpServer
{
    public class HealthMonitorConnectionKeeper : IDisposable
    {
        private const string TRACE_FOLDER_NAME = "Monitor Sistema\\Conexoes";

        private const int TIME_TO_PROCESS_NEXT_MESSAGE_BLOCK = 25;
        private const int TIME_TO_PROCESS_NEXT_MESSAGE_ON_STREAM = 1;
        private const int TIME_TO_WAIT_HEARTBEAT = 10000;
        private const int TIME_TO_SEND_HEARTBEAT = 5000;
        private const int TCP_BLOCK_SIZE = 2048;

        private Socket _sckClient;

        private string remoteEndPoint;

        private TraceWriter _traceWriter;

        private Queue<HealthMonitorMessage> _queueReceivedMessages;
        private Queue<byte[]> _queueMessages2Send;

        private bool _keepRunning;
        private bool _closing;
        private bool _heartBeatReceived;
        private int _threadsExited = 0;
        private int _threadsStarted = 5;

        public delegate void CloseHandler(string remoteEndPoint);
        public event CloseHandler OnCloseEvent;

        public HealthMonitorConnectionKeeper(Socket sckClient)
        {
            _keepRunning = true;
            _closing = false;

            remoteEndPoint = sckClient.RemoteEndPoint.ToString();
            _traceWriter = Tracer.GetInstance(TRACE_FOLDER_NAME, "Comunicador TCP " + sckClient.RemoteEndPoint.ToString());

            _sckClient = sckClient;
            _sckClient.ReceiveTimeout = 10000;
            _sckClient.SendTimeout = 10000;

            _queueReceivedMessages = new Queue<HealthMonitorMessage>();
            _queueMessages2Send = new Queue<byte[]>();

        }

        public void Run()
        {
            Thread thrHeartBeatSender = new Thread(new ThreadStart(HeartBeatSender));
            thrHeartBeatSender.Name = "[1] Thread HeartBeat Sender";
            thrHeartBeatSender.Start();

            Thread thrHeartBeatMonitor = new Thread(new ThreadStart(HeartBeatMonitor));
            thrHeartBeatMonitor.Name = "[2] Thread HeartBeat Monitor";
            thrHeartBeatMonitor.Start();

            Thread thrReceivedMessageProcessor = new Thread(new ThreadStart(ReceivedMessageProcessor));
            thrReceivedMessageProcessor.Name = "[3] Thread Message Processor";
            thrReceivedMessageProcessor.Start();

            Thread thrMessageReceiver = new Thread(new ThreadStart(MessageReceiver));
            thrMessageReceiver.Name = "[4] Thread Message Receiver - ";
            thrMessageReceiver.Start();

            Thread thrMessageSender = new Thread(new ThreadStart(MessageSender));
            thrMessageSender.Name = "[5] Thread Message Sender - ";
            thrMessageSender.Start();

        }

        public void Send(byte[] buffer2Send)
        {
            lock (_queueMessages2Send)
                _queueMessages2Send.Enqueue(buffer2Send);
        }

        public void HeartBeatSender()
        {
            _keepRunning = true;
            _threadsStarted--;

            _traceWriter.Trace("[1] Thread HeartBeatSender started...");
            byte[] heartBeat = GetHeartBeat();
            while (_keepRunning)
            {
                try
                {
                    this.Send(heartBeat);
                    _traceWriter.Trace("Enviou Heart Beat para o Client!", TraceType.Optional);
                }
                catch (Exception ex)
                {
                    _traceWriter.Trace("Thread HeartBeatSender stoped!");
                    try
                    {
                        CloseSocketClient();
                    }
                    catch (Exception ex1)
                    {
                        _traceWriter.Trace("Erro ao parar a HeartBeatSender!");
                        _traceWriter.Trace(ex1.Message);
                        _traceWriter.Trace(ex1.StackTrace);
                    }
                }
                Sleeper.Sleep(TIME_TO_SEND_HEARTBEAT, IsRunning);
            }

            _traceWriter.Trace("[1] Thread HeartBeatSender stoped!");

            _threadsExited++;
        }

        private void HeartBeatMonitor()
        {
            int cont = 0;

            _keepRunning = true;
            _threadsStarted--;

            _traceWriter.Trace("[2] Thread HeartBeatMonitor started!");

            while (!_closing)
            {
                _heartBeatReceived = false;

                Sleeper.Sleep(TIME_TO_WAIT_HEARTBEAT, IsHeartBeatReceived);

                if (_heartBeatReceived)
                    cont = 0;
                else
                    cont++;

                if (cont >= 3)
                {
                    _traceWriter.Trace("Vai desconectar por não receber HeartBeat!");
                    CloseSocketClient();
                }
            }
            _traceWriter.Trace("[2] Thread HeartBeatMonitor stoped!");

            _threadsExited++;
        }

        private void MessageReceiver()
        {
            _threadsStarted--;

            _traceWriter.Trace("[4] Thread MessageReceiver started...");

            try
            {

                while (!_closing)
                {
                    HealthMonitorMessage msgRcvd = new HealthMonitorMessage();

                    try
                    {
                        _traceWriter.Trace("Receberá [" + HealthMonitorMessage.HEADERSIZE + "] bytes", TraceType.Optional);
                        msgRcvd.header = ReceiveFromSocket(HealthMonitorMessage.HEADERSIZE);
                        _traceWriter.Trace("msgRcvd.header[" + Encoding.Default.GetString(msgRcvd.header) + "]", TraceType.Optional);
                        _traceWriter.Trace("Receberá [" + HealthMonitorMessage.MESSAGETYPESIZE + "] bytes", TraceType.Optional);
                        msgRcvd.messageType = ReceiveFromSocket(HealthMonitorMessage.MESSAGETYPESIZE);
                        _traceWriter.Trace("msgRcvd.messageType[" + Encoding.Default.GetString(msgRcvd.messageType) + "]", TraceType.Optional);
                        int bodySize = Int32.Parse(Encoding.Default.GetString(msgRcvd.header));
                        _traceWriter.Trace("bodySize[" + bodySize + "]", TraceType.Optional);
                        _traceWriter.Trace("Receberá [" + bodySize + "] bytes", TraceType.Optional);
                        msgRcvd.body = ReceiveFromSocket(bodySize);
                        _traceWriter.Trace("msgRcvd.body[" + Encoding.Default.GetString(msgRcvd.body) + "]", TraceType.Optional);
                        if (!_closing)
                        {
                            lock (_queueReceivedMessages)
                                _queueReceivedMessages.Enqueue(msgRcvd);
                        }
                    }
                    catch (SocketException sckEx)
                    {
                        if (sckEx.SocketErrorCode != SocketError.WouldBlock && sckEx.SocketErrorCode != SocketError.TimedOut)
                        {
                            _traceWriter.Trace("Socket:[" + sckEx.ErrorCode + "]");
                            _traceWriter.Trace("Socket:[" + sckEx.SocketErrorCode.ToString() + "]");
                            _traceWriter.Trace("Socket:[" + sckEx.Message + "]");
                            _traceWriter.Trace("Socket:[" + sckEx.NativeErrorCode + "]");
                            CloseSocketClient();
                        }
                    }
                    catch (Exception ex)
                    {
                        _traceWriter.Trace("ERRO :[" + ex.Message + "]");
                        _traceWriter.Trace(ex.StackTrace);
                        CloseSocketClient();
                    }

                    if (!_closing)
                        Sleeper.Sleep(TIME_TO_PROCESS_NEXT_MESSAGE_ON_STREAM, IsRunning);
                }
            }
            catch (Exception ex)
            {
                CloseSocketClient();
                _traceWriter.Trace("ERRO :[" + ex.Message + "]");
                _traceWriter.Trace(ex.StackTrace);
            }


            _traceWriter.Trace("[4] Thread MessageReceiver stoped...");

            _threadsExited++;
        }

        private void MessageSender()
        {
            _keepRunning = true;
            _threadsStarted--;

            _traceWriter.Trace("[5] Thread MessageSender started ...");

            try
            {

                while (!_closing)
                {
                    lock (_queueMessages2Send)
                    {
                        while (_queueMessages2Send != null && _queueMessages2Send.Count > 0 && !_closing)
                        {
                            try
                            {
                                byte[] message;
                                message = _queueMessages2Send.Dequeue();
                                SendToSocket(message);
                                message = null;
                            }
                            catch (Exception ex)
                            {
                                CloseSocketClient();

                                _traceWriter.Trace("Erro ao tentar dar Send na mensagem.");
                                _traceWriter.Trace(ex.StackTrace);
                            }
                        }
                    }

                    Sleeper.Sleep(TIME_TO_PROCESS_NEXT_MESSAGE_BLOCK, IsRunning);
                }
            }
            catch (Exception ex)
            {
                _traceWriter.Trace("ERRO :[" + ex.Message + "]");
                _traceWriter.Trace(ex.StackTrace);
            }

            _traceWriter.Trace("[5] Thread MessageSender stoped...");

            _threadsExited++;
        }

        private void ReceivedMessageProcessor()
        {
            _threadsStarted--;
            _traceWriter.Trace("[3] Thread ReceivedMessageProcessor started...");
            while (_keepRunning)
            {
                try
                {
                    int cont = 0;
                    lock (_queueReceivedMessages)
                    {
                        cont = _queueReceivedMessages.Count;
                    }
                    while (_queueReceivedMessages != null && cont > 0 && _keepRunning)
                    {
                        HealthMonitorMessage msgRcvd;
                        lock (_queueReceivedMessages)
                        {
                            msgRcvd = _queueReceivedMessages.Dequeue();
                            cont--;
                        }

                        if (!_closing)
                        {
                            TrataMensagem(msgRcvd);
                        }
                        msgRcvd = null;
                    }
                    if (_keepRunning)
                        Sleeper.Sleep(TIME_TO_PROCESS_NEXT_MESSAGE_BLOCK, IsRunning);
                }
                catch (Exception ex)
                {
                    _traceWriter.Trace("ERRO :[" + ex.Message + "]");
                    _traceWriter.Trace(ex.StackTrace);
                }
            }
            _traceWriter.Trace("[3] Thread ReceivedMessageProcessor stoped...");
            _threadsExited++;
        }

        private void SendToSocket(byte[] buffer)
        {
            if (!_closing)
            {
                _sckClient.Send(buffer, SocketFlags.None);
            }
        }

        private byte[] ReceiveFromSocket(int bytesQuantity)
        {

            int bytesReceived = 0;

            byte[] buffer = new byte[bytesQuantity];
            try
            {
                _traceWriter.Trace("Irá Receber[" + bytesQuantity + "] bytes" , TraceType.Optional);
                if(!_closing)
                {
                    while (bytesReceived < bytesQuantity && !_closing)
                    {
                        _traceWriter.Trace("Entrou no While [ReceiveFromSocket]" , TraceType.Optional);
                        if (!_closing)
                        {
                            bytesReceived = bytesReceived + _sckClient.Receive(buffer, bytesQuantity - bytesReceived, SocketFlags.None);
                            _traceWriter.Trace("Recebidos [" + bytesReceived + "] bytes", TraceType.Optional);
                        }

                    }
                }
            }
            catch (Exception ex)
            {
                _traceWriter.Trace("ERRO ReceiveFromSocket Msg[" + ex.Message + "]");
            }
            return buffer;
        }

        private byte[] GetHeartBeat()
        {
            byte[] ret;
            ret = Encoding.Default.GetBytes(HealthMonitorMessage.HEARTBEATMESSAGE);
            return ret;
        }

        private void TrataMensagem(HealthMonitorMessage msgRcvd)
        {
            int intMsgType;
            intMsgType = Int16.Parse(Encoding.Default.GetString(msgRcvd.messageType));
            _traceWriter.Trace("intMsgType[" + intMsgType + "]", TraceType.Optional);
            if (((int)HealthMonitorMessageType.HeartBeat) == intMsgType)
                SetHeartBeatReceived();
        }

        private void SetHeartBeatReceived()
        {
            _traceWriter.Trace("Recebeu Heart Beat do Client!", TraceType.Optional);
            _heartBeatReceived = true;
        }

        private bool IsHeartBeatReceived()
        {
            return !_heartBeatReceived && _keepRunning;
        }

        private bool IsRunning()
        {
            return _keepRunning;
        }

        private void CloseSocketClient()
        {
            if (!IsThreadsNotStarted())
            {
                _traceWriter.Trace("Aguardando Threads serem abertas por até 2 minutos");

                WaitForThreadsStarts();

                if (IsThreadsNotStarted())
                    _traceWriter.Trace("Não está mais aguardando, threads NÃO foram abertas...");
                else
                    _traceWriter.Trace("Não está mais aguardando, threads todas abertas !!!");
            }

            if (!_closing)
            {
                _closing = true;

                if (_sckClient != null)
                {
                    try
                    {
                        _sckClient.Shutdown(SocketShutdown.Both);
                        _sckClient.Disconnect(false);
                        _sckClient.Close();
                        _traceWriter.Trace("Fechou Socket!");
                    }
                    catch (Exception ex)
                    {
                        try
                        {
                            _traceWriter.Trace("ERRO : [" + ex.Message + "]");
                            _traceWriter.Trace(ex.StackTrace);
                        }
                        catch (Exception) { }
                    }

                    _sckClient = null;
                }

                new Thread(new ThreadStart(DisparaEventoDisconnect)).Start();
            }
        }

        private void DisparaEventoDisconnect()
        {
            try
            {
                if (OnCloseEvent != null)
                    OnCloseEvent(remoteEndPoint);
            }
            catch (Exception)
            {

            }
        }

        public void Dispose()
        {
            _keepRunning = false;

            _traceWriter.Trace("Aguardando Threads serem fechadas por 10 segundos");
            WaitForThreadsExit();

            if (!IsThreadsNotExited())
                _traceWriter.Trace("Threads fechadas...");
            else
                _traceWriter.Trace("Não fechou todas as Threads !!!");

            if (_traceWriter != null)
                _traceWriter.Trace("Finalizando a conexão...");

            try
            {
                _traceWriter.Trace("Limpando fila de Mensagens Recebidas...");

                if (_queueReceivedMessages != null)
                {
                    lock (_queueReceivedMessages)
                        _queueReceivedMessages.Clear();

                    _queueReceivedMessages = null;
                }

                _traceWriter.Trace("Limpando fila de Mensagens para Enviar...");

                if (_queueReceivedMessages != null)
                {
                    lock (_queueMessages2Send)
                        _queueMessages2Send.Clear();

                    _queueMessages2Send = null;
                }
            }
            catch (Exception ex)
            {
                if (_traceWriter != null)
                {
                    _traceWriter.Trace("ERRO : [" + ex.Message + "]");
                    _traceWriter.Trace(ex.StackTrace);
                    _traceWriter.Trace("Saindo...");
                }
            }

            _traceWriter.Trace("Connection Keeper Destroyed.");
            _traceWriter.Close();
            _traceWriter = null;

            OnCloseEvent = null;
        }

        public bool IsNotClosing()
        {
            return !_closing;
        }

        public void WaitForThreadsExit()
        {
            Sleeper.Sleep(10000, IsThreadsNotExited);
        }

        public void WaitForThreadsStarts()
        {
            Sleeper.Sleep(120000, IsThreadsNotStarted);
        }

        public bool IsThreadsNotExited()
        {
            if (_threadsExited >= 5)
                return false;
            else
                return true;
        }

        public bool IsThreadsNotStarted()
        {
            if (_threadsStarted > 0)
                return false;
            else
                return true;
        }
    }
}
